#!/usr/bin/env python

__docformat__ = 'restructuredtext'

import os
import random
import ctypes

from pyglet import clock
from pyglet.gl import *
#from pyglet.glu import *
from pyglet.ext.euclid import *
from pyglet.window import *
from pyglet.event import *
from gobject import gobject
from pyglet.ext.scene2d.textsprite import *
from pyglet.font import *
#from pyglet.window import key
from pyglet.ext.model.obj import OBJ
from selection import *

# ODE stuff
import ode
# Bitfield to identify pieces
PIECES = 1L
world = ode.World()
world.setGravity( (0,-9.81,0) )
# Create a space object
space = ode.Space()
# Create a plane geom which prevent the objects from falling forever
floor = ode.GeomPlane(space, (0,1,0), 0)
# A joint group for the contact joints that are generated whenever
# two bodies collide
contactgroup = ode.JointGroup()

# Selection tools
selectPlane = ode.GeomPlane(space, (0,1,0), 0)
selectPlane.setCollideBits(0)
selectPlane.setCategoryBits(0)
selectRay = ode.GeomRay(space, rlen=1.0)
selectRay.setCollideBits(0)
selectRay.setCategoryBits(0)
selectDrag = ode.GeomSphere(space, 0.5)
selectDrag.setPosition((0., 1., 0.))
#selectDrag.setBody(ode.environment)
selectDrag.setCollideBits(0)
selectDrag.setCategoryBits(0)
body = ode.Body(world)
M = ode.Mass()
M.setSphere(1000, 1.0)
body.setMass(M)
# Set parameters for drawing the body
body.shape = "sphere"
geom = ode.GeomSphere(space, 1.0)
selectDrag.setBody(body)
selectedObj = None
selectTarget = None

start, end = [0., 0., 0.], [0., 0., 0.]
def updateSelectionRay(x, y):
    global selectRay
#    setupCamera()
    start, end = generateSelectionRay(x, y)
    selectRay.set(start, [ b - a for a, b in zip(start, end) ])
    
displayfps = True
displayconsole = True
text = "Welcome to Geoffrey.\n"
window = pyglet.window.Window(width=1024, height=768, resizable=True)
font = load('Arial', 14)
displaycolour = (1, 1, 1, 0.5)
sprite = TextSprite(font, text, color=displaycolour)
field = TextSprite(font, '', color=(1, 1, 0.8, 0.5))
sprite.x = 10
sprite.y = 24
field.x = 10
field.y = 10

#c = clock.Clock(60)
fps = clock.ClockDisplay(font=font, color=displaycolour)
#window.push_handlers(fps)

#Display rotation globals
horizontal = 0
vertical = 0

threefv = ctypes.c_float * 3
threedv = ctypes.c_double * 3
fourfv = ctypes.c_float * 4
fouriv = ctypes.c_int * 4
sixteendv = ctypes.c_double * 16

BLACK = threefv(0.2, 0.2, 0.2)
WHITE = threefv(0.9, 0.9, 0.9)
SIZE = 4 # Board square size

glLightfv(GL_LIGHT0, GL_POSITION, fourfv(20, 50, 20, 0))
glLightfv(GL_LIGHT0, GL_AMBIENT, fourfv(0.5, 0.5, 0.5, 1.0))
glLightfv(GL_LIGHT0, GL_DIFFUSE, fourfv(0.8, 0.8, 0.8, 1.0))
glLightfv(GL_LIGHT0, GL_SPECULAR, fourfv(0.8, 0.8, 0.8, 1.0))
# ColorMaterial use inspired by: http://www.sjbaker.org/steve/omniv/opengl_lighting.html
glEnable ( GL_COLOR_MATERIAL ) ;
glColorMaterial ( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE ) ;

def log(string):
    sprite.text += string + "\n"
    
def resize(width, height):
    global sprite, fps
    print 'resize', width, height
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(60., float(width)/height, 1., 100.)
    glMatrixMode(GL_MODELVIEW)
    sprite.width = width
    field.width = width
    fps.label.x = width - 50
    fps.label.y = height - 20
    
window.push_handlers(on_resize=resize)
 

def object_select(button, x, y, modifiers):
    global selectedObj, selectRay, selectPlane, selectOffset, selectTarget

    # Selection is with button 1
    if button != 1:
        return
    
    updateSelectionRay(x, y)
    # Iterate over all pieces, collect all collisions
    for piece in pieces:
        contacts = ode.collide(selectRay, piece.geometry)
        if contacts:
            selectedObj = piece
#            selectedObj.geometry.getBody().setGravityMode(False)
            # Create target and offset info
            selectTarget = Point3(*contacts[0].getContactGeomParams()[0])
            selectOffset = selectTarget - Point3(*selectedObj.geometry.getPosition())
            # Set the height of the selection place to the contact point on the target
            selectPlane.setParams((0, 1, 0), selectTarget.y)
    else:
        return
    
window.push_handlers(on_mouse_press=object_select)

def mouse_drag(x, y, dx, dy, buttons, modifiers):
    # Rotate the view based on mouse movement
    global horizontal, vertical, selectRay, selectedObj, selectPlane, selectOffset, selectTarget
    if buttons & 4:
        horizontal += dx
        vertical -= dy
    #    clamp rotation?
    #    if vertical < 0:
    #        vertical = 0
    #    if vertical > 45:
    #        vertical = 45

    if selectedObj:
        updateSelectionRay(x, y)
        contacts = ode.collide(selectRay, selectPlane)
        if contacts:
            intersect = Point3(*contacts[0].getContactGeomParams()[0])
            selectTarget = intersect + selectOffset

window.push_handlers(on_mouse_drag=mouse_drag)

def object_release(button, x, y, modifiers):
    global selectedObj
    if button == 1 and selectedObj is not None:
        selectedObj = None
window.push_handlers(on_mouse_release=object_release)

# Swap this back in if we ever want to (for instance) highlight mouseover pieces
#def mouse_movement(x, y, dx, dy):
#    updateSelectionRay(x, y)
#    
#window.push_handlers(on_mouse_motion=mouse_movement)

quick_file = 'quick.save'

def save_state(filename=quick_file):
    # Save the current game places
    handle = None
    try:
        # XXX Make this OS safe
        handle = open('saves/' + filename, 'w')
    except IOError:
        log(filename + ' not found')
        return
    for piece in pieces:
        pass
        values = piece.geometry.getPosition() + piece.geometry.getBody().getQuaternion()
        handle.write(','.join([str(value) for value in values]) + "\n")
    handle.close()
    log(filename + ' saved')

def load_state(filename=quick_file):
    # Load the current game places
    handle = None
    try:
        # XXX Make this OS safe
        handle = open('saves/' + filename, 'r')
    except IOError:
        log(filename + ' not found')
        return
    data = handle.readlines()
    handle.close()
    for piece, line in zip(pieces, data):
        values = [float(x) for x in line.strip().split(',')]
        piece.geometry.setPosition(values[:3])
        piece.geometry.getBody().setQuaternion(values[3:])
        piece.geometry.getBody().setLinearVel([0., 0., 0.])
    log(filename + ' loaded')
    
def on_text(text):
    field.text += text.strip("\r")

def on_key_press(symbol, modifiers):
    global displayfps, displayconsole
    if symbol == key.S and (modifiers & key.MOD_CTRL):
        save_state()
    elif symbol == key.L and (modifiers & key.MOD_CTRL):
        load_state()
    elif symbol == key.F and (modifiers & key.MOD_CTRL):
        displayfps = not displayfps
    elif symbol == key.SLASH:
        displayconsole = not displayconsole
    elif symbol == key.BACKSPACE:
        field.text = field.text[:-1]
    elif symbol == key.RETURN and field.text:
        sprite.text += field.text + "\n"
        field.text = ''
    else:
        return EVENT_UNHANDLED

window.push_handlers(on_text)
window.push_handlers(on_key_press)
    
def draw_board():
    global SIZE, BLACK, WHITE
    glNormal3f(.0, 1., .0)
    for x in range(-4*SIZE, 4*SIZE, SIZE):
        for y in range(-4*SIZE, 4*SIZE, SIZE):
            if ((x + y) / SIZE) % 2:
                glColor3f(*BLACK)
            else:
                glColor3f(*WHITE)
            glBegin(GL_QUADS)
            glVertex3f(x, 0., y)
            glVertex3f(x+SIZE, 0., y)
            glVertex3f(x+SIZE, 0., y+SIZE)
            glVertex3f(x, 0., y+SIZE)
            glEnd()
            
def setupCamera():
    global vertical, horizontal
    glLoadIdentity()
    gluLookAt(0, 30, 30, 0, -5, 0, 0, 1, 0)

    glRotatef(vertical, 1, 0, 0)
    glRotatef(horizontal, 0, 1, 0)
    
 # Collision callback
def near_callback(args, geom1, geom2):
    """Callback function for the collide() method.

    This function checks if the given geoms do collide and
    creates contact joints if they do.
    """
    global selectedObj
    # Check if the objects do collide
    contacts = ode.collide(geom1, geom2)

    # Create contact joints
    world,contactgroup = args
    for c in contacts:
        c.setBounce(0.01)
        c.setMu(5000)
        j = ode.ContactJoint(world, contactgroup, c)
#        if selectedObj and selectedObj.geometry is geom1:
#            j.attach(ode.environment, geom2.getBody())
#        elif selectedObj and selectedObj.geometry is geom2:
#            j.attach(geom1.getBody(), ode.environment)
#        else:
#            j.attach(geom1.getBody(), geom2.getBody())
        j.attach(geom1.getBody(), geom2.getBody())
        
resize(window.width, window.height)
glClearColor(0, 0, 0, 0)
setupCamera()

bordrand = lambda: (random.random()-0.5)*7*SIZE
pawn = OBJ(os.path.join(os.path.split(__file__)[0], 'models/pawn.obj'))
bishop = OBJ(os.path.join(os.path.split(__file__)[0], 'models/bishop.obj'))
knight = OBJ(os.path.join(os.path.split(__file__)[0], 'models/knight.obj'))
rook = OBJ(os.path.join(os.path.split(__file__)[0], 'models/rook.obj'))
#queen = OBJ(os.path.join(os.path.split(__file__)[0], 'models/queen.obj'))
#king = OBJ(os.path.join(os.path.split(__file__)[0], 'models/king.obj'))

pieces = []
for i in range(0, 32):
    # Default to pawn, then set the piece model...
    model = pawn
    if i % 16 in (0, 1):
        model = bishop
    if i % 16 in (2, 3):
        model = knight
    if i % 16 in (4, 5):
        model = rook
#    if i in (6, 7):
#        model = queen
#    if i in (8, 9):
#        model = king

    # Default to black, then set the colour...
    colour = BLACK
    if i % 2:
        colour = WHITE
        
#    TriMesh support disabled?
#    def collectTriangles(model):
#        for face in model.faces:
#            vertlist = face[0]
#            yield [[vertlist[0], vertlist[i], vertlist[i+1]] for i in range(1, len(vertlist)-1)]
#
#    ode.TriMeshData().build(model.vertices, list(collectTriangles(model)))
    
    # Create body
    body = ode.Body(world)
    M = ode.Mass()
    M.setBox(1000, 1., 2., 1.)
    body.setMass(M)
    # Set parameters for drawing the body
    body.shape = "box"
    body.boxsize = (2., 4., 2.)
    # Create a box geom for collision detection
    geom = ode.GeomBox(space, lengths=body.boxsize)
    geom.setCollideBits(PIECES)
    geom.setCategoryBits(PIECES)
    geom.setBody(body)
    x = (i % 8 - 4) * 4
    z = (i // 8 - 4) * 4
    body.setPosition([x, 2., z])
    body.setRotation([0.] * 9)

    pieces.append(gobject(geom, model, colour))
    
load_state('static.save')

while not window.has_exit:
    dt = clock.tick()
    window.dispatch_events()

    glBegin(GL_POINTS)
    glColor3f(1.0, 0.0, 0.0)
    glVertex3f(*selectDrag.getPosition())
    glEnd()
    
    glEnable(GL_LIGHT0)
    glEnable(GL_LIGHTING)
    glEnable(GL_DEPTH_TEST)

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    # Render everything...
    glPushMatrix()
    draw_board()
    
# Draw the selection ray...
    if selectedObj is not None:
        glBegin(GL_LINES)
        glColor3f(1.0, 0.0, 0.0)
        points = selectRay.get()
        start = Point3(*points[0])
        dir = Point3(*points[1])
        end = start + dir
        glVertex3f(*start)
        glVertex3f(*end)
        glEnd()

    for i in range(0, 32):
        piece = pieces[i]

        glPushMatrix()
        #glTranslatef(*piece.getPosition())
        x,y,z = piece.geometry.getPosition()
        R = piece.geometry.getRotation()
        rot = sixteendv(R[0], R[3], R[6], 0.,
               R[1], R[4], R[7], 0.,
               R[2], R[5], R[8], 0.,
               x, y, z, 1.0)
            
        glMultMatrixd(rot)
        glColor3f(*piece.colour)
        glTranslatef(0., -2., 0.)
        piece.model.draw()
        glPopMatrix()
        
    glPopMatrix()

    # Simulate
    n = 1

    for i in range(n):
        # Correct the rotation of the selected object
        if selectedObj is not None:
            # Different dragging tricks...    
            curpos = Point3(*selectedObj.geometry.getPosition())
            curvel = Point3(*selectedObj.geometry.getBody().getLinearVel())
            newpos = selectTarget + selectOffset
            # Future pos stuff inspired by:
            # http://opende.sourceforge.net/wiki/index.php/HOWTO_thrust_control_logic        
            futpos = curpos + curvel
            diff = newpos - futpos
            speed = diff * 10000
#            selectedObj.geometry.getBody().setPosition(newPos)
#            selectedObj.geometry.getBody().setLinearVel(speed)
            selectedObj.geometry.getBody().setForce(speed)
            
            rot = selectedObj.geometry.getRotation()
            rot = [0.,] * 9
            selectedObj.geometry.setRotation(rot)
            selectedObj.geometry.getBody().setAngularVel(rot)
            
        # Detect collisions and create contact joints
        space.collide((world,contactgroup), near_callback)

        # Simulation step
        world.step(dt/n)

        # Remove all contact joints
        contactgroup.empty()

    
    # HUD rendering translation based on this: http://www.oreillynet.com/network/2000/06/23/magazine/cube.c
    # Found from here: http://www.oreillynet.com/pub/a/network/2000/06/23/magazine/opengl_build.html?page=2    
    # Move back to the origin (for the text, below).
    glLoadIdentity()
    # We need to change the projection matrix for the text rendering.  
    glMatrixMode(GL_PROJECTION)
    # But we like our current view too; so we save it here.
    glPushMatrix()
    # Now we set up a new projection for the text.
    glLoadIdentity()
    glOrtho(0, window.width, 0, window.height, -1.0, 1.0)
    glDisable(GL_TEXTURE_2D);
    glDisable(GL_LIGHTING);
    # We don't want depth-testing either.
    glDisable(GL_DEPTH_TEST); 
    if displayconsole:
        sprite.y = sprite.height
        field.y = field.height
        sprite.draw()
        field.draw()
    if displayfps:
        fps.draw()
    glPopMatrix()
    glMatrixMode(GL_MODELVIEW)

    window.flip()

    setupCamera()
    
save_state('static.save')
